package cyce.file;

import java.util.*;

import org.jdom.Element;

/**
 * Directory represents a directory stored in the distributed file system
 * A Directory is a File that can contain other files and does not have a hash
 * A root Directory is a Directory that does not have a parent
 * @author Colin Rhodes
 *
 */
public class Directory extends File {

	private static final long serialVersionUID = -8124769521722575884L;
	private ArrayList<File> children;
	
	/**
	 * @param parent The Directory containing this directory (null for root directory)
	 * @param directoryName The name of this directory in the distr file system (not local filename)
	 * @param parentPath The path of the directory into which to insert the Directory
	 */
	protected Directory(String directoryName, String parentPath) {
		super(directoryName, parentPath, "");
		this.children = new ArrayList<File>();
	}
	
	/**
	 * Overridden constraints on constructor parameters from File class
	 */
	@Override
	protected boolean validateAll(String name, String parentPath, String hash) {
		if (!validateName(name)) 
			throw new IllegalArgumentException("Directory name must be a non-null string not containing '/'");
		if (!validateParentPath(parentPath))
			throw new IllegalArgumentException("invalid path");
		if (!validateNameAndParentPath(name, parentPath))
			throw new IllegalArgumentException("Directory name of may only be empty for root directories.");

		return true;
	}
	
	/**
	 * Overridden to allow empty name for root dir
	 */
	@Override
	protected boolean validateName(String name) {
		if (name == null) return false;
		if (name.equals("")) return true; // allow empty name for root dir
		if (name.contains("/")) return false;
		return true;
	}
	
	protected boolean validateNameAndParentPath(String name, String parentPath) {
		// only time name can (and must) be null is when parentPath is null (root dir)
		if (name.equals("") && parentPath != null) return false;
		if (!name.equals("") && parentPath == null) return false;
		return true;
	}
	



	/**
	 * Create a Directory object from a JDOM Element
	 * @param element Element representing a directory
	 * @param parentPath Path of parent directory (use null to grab from XML)
	 * @return The deserialized Directory object
	 */
	protected static Directory deserialize(Element element, String parentPath) {
		if (element.getName() != "directory")
			throw new IllegalArgumentException("Element does not represent a valid Directory");
		
		// deserialize this directory
		String name = element.getAttributeValue("name");
		String path = parentPath;
		if (path == null) {
			path = element.getAttributeValue("path");
			if (path.equals("null")) path = null;
		}

		Directory dir = new Directory(name, path);
		
		// deserialize children
		for (Object obj : element.getChildren()) {
			Element childElem = (Element)obj;
			if (childElem.getName() == "directory") {
				Directory newDir = Directory.deserialize(childElem, dir.getPath());
				dir.children.add(newDir);
			}
			else if (childElem.getName() == "file") {
				File newFile = File.deserialize(childElem, dir.getPath());
				dir.children.add(newFile);
			}
		}
		
		return dir;
	}
	
	@Override
	public String toString() {
		if (this.isRoot())
			return "Remote File System";
		else
			return this.getName();
	}
	/**
	 * List the directory structure for testing purposes
	 * @return
	 */
	protected String list(String prefix) {
		String list = prefix + this.getName() + "{";
		for (File file : this.children) {
			list += "\n" + file.list(prefix + "  ");
		}
		return list + "\n" + prefix + "}";
	}
	
	/**
	 * Get the absolute path of the File (as stored in distributed file system).
	 * The path includes the filename.
	 * The path of the root directory is "/".
	 * The path of all directories ends in "/".
	 * @return The absolute path of this file
	 */
	public String getPath() {
		if (this.isRoot()) {
			return "/";
		}
		else {
			return this.getParentPath() + this.getName() + "/";
		}
	}
	
	/**
	 * Determine whether a path refers to this or a child of this.
	 * Does not determine whether such a child exists.
	 * @return
	 */
	public boolean isParentOfPath(String path) {
		if (path.indexOf(this.getPath()) == 0)
			return true;
		else
			return false;
	}
	
	/**
	 * Get whether this Directory is the root directory
	 * @return True if this Directory is a root directory
	 */
	@Override
	public boolean isRoot() {
		return (this.getParentPath() == null);
	}
	
	/**
	 * Determine whether this directory contains a File as a direct child
	 * @param name Name of the File to look for
	 * @return Whether this directory contains the File
	 */
	public boolean containsFile(String name) {
		for (File file : this.children) {
			if (file.getName().equals(name))
				return true;
		}
		return false;
	}
	
	/**
	 * Add a Directory as a child
	 * @param name The name of the new Directory
	 * @return The new Directory
	 */
	protected Directory addDirectory(String name) {
		if (!validateName(name)) 
			throw new IllegalArgumentException("Directory name must be a non-null string not containing '/'");
		if (!validateNameAndParentPath(name, this.getPath()))
			throw new IllegalArgumentException("Directory name of may only be empty for root directories.");
		if (this.containsFile(name))
			throw new IllegalArgumentException("A file or directory with the requested name already exists.");
		
		Directory directory = new Directory(name, this.getPath());
		this.children.add(directory);
		return directory;
	}
	
	/**
	 * Add a File as a child
	 * @param name The name of the new File
	 * @return The new File
	 */
	protected File addFile(String name, String hash) {
		if (!super.validateName(name)) 
			throw new IllegalArgumentException("File name must be a non-empty string not containing '/'");
		if (!super.validateHash(hash))
			throw new IllegalArgumentException("hash cannot be null");
		if (this.containsFile(name))
			throw new IllegalArgumentException("A file or directory with the requested name already exists.");
		
		File file = new File(name, this.getPath(), hash);
		this.children.add(file);
		return file;
	}
	
	/**
	 * Remove a File as a descendant of this Directory
	 * @param path The path of the File to remove
	 */
	protected void removeFile(String path) {
		for (File file : this.children) {
			// if path refers to a child of this, remove it
			if (file.getPath().equals(path)) {
				this.children.remove(file);
				return;
			}
			
			// if path refers to a descendant of a child Directory, 
			// pass on the instruction to remove
			if (file instanceof Directory) {
				Directory dir = (Directory)file;
				if (dir.isParentOfPath(path)) {
					dir.removeFile(path);
					return;
				}
			}
		}
	}
	
	/**
	 * Represent the data of this Directory in a JDOM Element
	 * @return A JDOM Element with the data of this Directory
	 */
	@Override
	protected Element serialize() {
		Element dir = new Element("directory");
		dir.setAttribute("name", this.getName());
		String parentPath = this.getParentPath();
		if (parentPath == null) parentPath = "null";
		dir.setAttribute("path", parentPath);
		for (File file : this.children) {
			dir.addContent(file.serialize());
		}
		return dir;
	}
	
	/**
	 * Get a File at a given path within this Directory, including this Directory
	 * @param path The absolute path of the File
	 * @return The File at the path (if found), or null
	 */
	@Override
	public File getFile(String path) {
		if (this.getPath().equals(path))
			return this;
		
		// search children
		for (File file : this.children) {
			File result = file.getFile(path);
			if (result != null) return result;
		}
		
		return null;
	}
	
	/**
	 * Get a File at a given path within this Directory by hash
	 * @param hash The hash of the File
	 * @return The File with the hash (if found), or null
	 */
	@Override
	public File getFileByHash(String hash) {
		for (File file : this.children) {
			File result = file.getFileByHash(hash);
			if (result != null) return result;
		}
		
		return null;
	}
	
	/**
	 * Get the children of this Directory
	 * @return
	 */
	public ArrayList<File> children() {
		return this.children;
	}

	/**
	 * Get the File at a given index within this Directory
	 * @param index
	 * @return
	 */
	public File getChildAt(int index) {
		return this.children.get(index);
	}

	/**
	 * Get the number of Files within this Directory
	 * @return
	 */
	public int getChildCount() {
		return this.children.size();
	}

	/**
	 * Get the index of a File within this Directory
	 * @param file
	 * @return
	 */
	public int getIndex(File file) {
		return this.children.indexOf(file);
	}
	
	/* (non-Javadoc)
	 * @see cyce.file.File#equals(java.lang.Object)
	 */
	public boolean equals(Object obj)
    {       
        if (obj != null && (obj.getClass().equals(this.getClass())))
        {
            Directory d = (Directory)obj;
            //assume path is a unique identifier
            return d.getPath().equals(this.getPath());
        }
        return false;
    }
}
